feat(speechify): add Speechify TTS plugin#1968
Open
luke-speechify wants to merge 15 commits into
Open
Conversation
🦋 Changeset detectedLatest commit: c01e24f The changes in this PR will be included in the next version bump. This PR includes changesets to release 37 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
luke-speechify
marked this pull request as draft
July 6, 2026 12:56
Add @livekit/agents-plugin-speechify: streaming text-to-speech with word-level timestamps via the official @speechify/api SDK. Chunks streamed input into sequential /audio/speech calls, emitting audio and timed transcripts per sentence (streaming + alignedTranscript). Maintained by Speechify (Speechify-AI/livekit-plugin-js).
luke-speechify
force-pushed
the
lo/add-speechify-plugin
branch
from
July 6, 2026 13:24
6869087 to
26fede2
Compare
luke-speechify
marked this pull request as ready for review
July 13, 2026 20:27
… drop model cast)
The SDK v3.0.0 renamed the auth constructor option from apiKey to token, and
v3.0.1's GetSpeechRequest.Model union now includes simba-3.0 and simba-3.2
natively so the plugin no longer needs to widen it via a cast.
- plugins/speechify/package.json: @speechify/api ^2.0.0 -> ^3.0.1
- plugins/speechify/src/tts.ts:
- rename public TTSOptions.apiKey -> token; pass through to
new SpeechifyClient({ token, baseUrl })
- update JSDoc + local variable + Omit-union for updateOptions
- drop the `opts.model as Speechify.GetSpeechRequest.Model` cast + comment;
v3.0.1's Model union already covers simba-english / simba-multilingual /
simba-3.0 / simba-3.2
- pnpm-lock.yaml: refreshed
Addresses Devin review feedback on livekit#1968. The base class SynthesizeStream.monitorMetrics gates all TTS metric emission on this.#startedHrTime being set, which only happens via markStarted(). Without it, TTFB, duration, and character-count metrics were silently dropped for all Speechify streaming synthesis, and USERDATA_TTS_STARTED_TIME never got stamped on frames so downstream transcription timing was degraded. Matches the elevenlabs / cartesia / resemble / deepgram plugin pattern.
Addresses Devin review findings on livekit#1968 that both Chunked and Synthesize streams close `this.queue` in their run() catch blocks before re-throwing. The base class already closes the queue in its own `finally` after run() returns, and it retries run() on retryable APIErrors - so closing on error made the next retry's `queue.put(...)` throw 'Queue is closed', turning transient network errors into permanent failures. Matches the guard the minimax plugin documents on its own catch.
Addresses Devin review finding on livekit#1968: calling `sentenceStream.close()` immediately after `endInput()` closes the tokenizer's output queue before its internal async loop has a chance to flush the last buffered sentence, so the final `queue.put(token)` throws 'Queue is closed' and consume() never sees the tail. Result: the last sentence of a TTS response could be silently dropped. Matches the neuphonic plugin pattern: only call endInput(), let the tokenizer close its own output queue when its input drains.
Author
|
hey @tinalenguyen! on this and livekit/agents#6327 was there anything else I needed to do? I noticed we had a new release go out with updates to a lot of vendors, but neither PR was included despite the changeset saying it would be |
- align auth arg with sibling plugins (was `token`, now `apiKey`)
- drop `client?: SpeechifyClient` option and `get client()` getter
- move the SDK call into an `@internal` `_synthesize` on TTS that returns
`{ audio, timed }` in LiveKit-owned types
- revert `package.json` author to LiveKit
- drop "maintained by Speechify" paragraph from the plugin README
verified `dist/tts.d.ts` no longer references any `Speechify*` SDK types.
Devin re-review (livekit#1968): - TTS.synthesize and ChunkedStream now accept + forward `abortSignal` to super() so external cancels reach the one-shot request path (matches the abstract signature in agents/src/tts/tts.ts and the cartesia plugin) - SynthesizeStream now emits `final: true` exactly once, on the last frame of the whole reply (previously per-sentence, which dropped sentences 2..N from the base-class metrics because flush() only records one pending metric text per reply). Matches elevenlabs/cartesia.
The buffer-one deferral in the previous commit meant `cumulativeDuration` lagged one frame behind actual produced audio, so every sentence after the first got a slightly early offset for its word marks (Devin re-review). Track the total emitted duration independently: advance `offsetSeconds` by the entire sentence's frame duration right after synthesis, before the deferral loop. The deferral still governs when `final: true` fires, but timestamps no longer share state with it.
ChunkedStream computed its timeout only from the optional connOptions param, leaving it undefined (no timeout) when synthesize() is called without explicit connOptions. SynthesizeStream already reads this.connOptions.timeoutMs (which the base class defaults to DEFAULT_API_CONNECT_OPTIONS) - mirror that here so a one-shot synthesis request can't hang indefinitely.
The SpeechifyClient constructor has no default-headers option, so the header has to be set per request via RequestOptions instead of at client construction. Centralized it in _synthesize() (the single call site both ChunkedStream and SynthesizeStream go through) so it can't be dropped by either path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
@livekit/agents-plugin-speechify— a Speechify TTS plugin for Agents JS with streaming audio and word-level timestamps, built on Speechify's official@speechify/apiSDK.There is currently no Speechify plugin in agents-js; this adds one following the existing plugin conventions (mirrors the
cartesiapackage layout).Details
Speechify's word-level speech marks are only returned by the batch
/audio/speechendpoint, soSynthesizeStreamchunks streamed input into per-sentence sequential/audio/speechcalls, emitting audio and aligned word timestamps as each sentence completes — near-streaming time-to-first-audio plusalignedTranscript.capabilities: { streaming: true, alignedTranscript: true }SynthesizeStream(sentence-chunked, cumulative word-mark offsets) +ChunkedStream(one-shot)@speechify/apivendor SDKturbo.jsonenv allowlist (SPEECHIFY_API_KEY)Maintenance & distribution
This plugin is maintained by Speechify and distributed by LiveKit as part of
livekit/agents-js. Speechify keeps a mirror of the plugin source at Speechify-AI/livekit-plugin-js for maintenance and issue triage, and proposes changes upstream here. The canonical, user-installable package is@livekit/agents-plugin-speechify, published by LiveKit — there is no separate Speechify-published package.Verification
pnpm build(ESM + CJS + type declarations) succeedspnpm lint(eslint/prettier/turbo) cleantsc --noEmitcleanHappy to adjust to match maintainer preferences.